home *** CD-ROM | disk | FTP | other *** search
Text File | 1990-06-24 | 23.7 KB | 490 lines | [TEXT/CCL ] |
- ; (c) Copyright 1990 by University of Massachusetts. All rights reserved.
- ; This software was conceived, designed, and written by Dan Suthers
- ; while supported by the National Science Foundation under grant number
- ; MDR 8751362, and by a fellowship from Apple Computer, Inc., Cupertino,
- ; CA. Partial support was also received from the Office of Naval Research
- ; under a University Research Initiative Grant, contract N00014-86-K-0764.
- ; Mr. Suthers created this software under his own initiative while in an
- ; academic relationship with the University of Massachusetts. The above
- ; copyright notice was a condition placed by University lawyers on approval
- ; of distribution of this software by Apple Computer, and is not meant to
- ; imply that this software was created in an employment or "work for hire"
- ; relationship between the University and Mr. Suthers.
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ; File: Rule-Forward.lisp
- ; Author: Dan Suthers
- ; Created: 19-Oct-88 21:57:32
- ; Modified: 22-Jun-90 02:18:10 (Dan Suthers)
- ; Language: Common Lisp
- ; Package: RULE
- ;
- ; Description: Rule-based reasoner built on the pattern matching facilities
- ; of DNET. Supports forward and backward reasoning.
- ;
- ; This file contains only the code for forward reasoning.
- ; See also Rule-Defs, Rule-Build, and Rule-Back.
- ; File RULES has documentation.
- ;
- ; (c) Copyright 1988, by Daniel D. Suthers
- ; Department of Computer and Information Science
- ; University of Massachusetts
- ; Amherst, Massachusetts 01003
- ;
- ; This software was conceived, designed, and written by Dan Suthers
- ; while supported by the National Science Foundation under grant number
- ; MDR 8751362, and by a fellowship from Apple Computer, Inc., Cupertino,
- ; CA. Partial support was also received from the Office of Naval Research
- ; under a University Research Initiative Grant, contract N00014-86-K-0764.
- ; I wish to acknowledge the generous support of Beverly Woolf, who obtained
- ; the above grants and encouraged me to pursue my own research interests in
- ; her lab. This work would not have been possible without the resources and
- ; stimulating environment of the Computer and Information Science department.
- ;
- ; Permission to use, modify, and distribute this software is granted subject
- ; to the following restrictions and understandings:
- ; 1. The file header, including this notice, shall be retained, and may be
- ; extended to include documentation of modifications to the software.
- ; 2. This material is for nonprofit educational and research purposes only.
- ; Users are requested, but not required, to inform Mr. Suthers of any
- ; noteworthy uses of this software.
- ; 3. Mr. Suthers and the University of Massachusetts make no warrantee or
- ; representation that the operation of this software will be error free,
- ; and are under no obligation to provide any services.
- ; 4. Any user of such software agrees to indemnify and hold harmless Mr.
- ; Suthers and the University of Massachusetts from all claims arising
- ; out of the use or misuse of this software, or arising out of any
- ; accident, injury, or damage whatsoever, and from all costs, counsel
- ; fees, and liabilities incurred in or about any such claim, action, or
- ; proceeding brought thereon.
- ; 5. All materials and reports developed as a consequence of the use of
- ; this software shall duly acknowledge such use, in accordance with
- ; the usual standards of acknowledging credit in academic research.
- ;
- ; Status: Working, subject to change.
- ;
- ; Changes:
- ; 30-Dec-88 :DELETE added to forward rules.
- ; 25-Mar-89 Cleanup; removed bogus comments prohibiting nested :AND.
- ;
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
- (in-package :RULE)
-
- (export '(
-
- forget-previous-bindings
- forward-chain
- infer-from-datum
- translate
-
- ))
-
- (require :Rule-Defs)
- (use-package :DNET)
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;;
- ;;; INTERNAL FUNCTIONS AND MACROS
- ;;;
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;; Little Helpers
-
- (eval-when (compile eval)
-
- ;; A version of this without bindings appears in RULE-BACK.
- (defmacro LISP-ESCAPE (bindings forms)
- `(progv (mapcar #'car ,bindings) (mapcar #'cdr ,bindings)
- ;; Want to return the last form evaluated, which dolist won't do.
- (do ((fptr ,forms (cdr fptr)))
- ((null (cdr fptr)) (eval (car fptr)))
- (declare (list fptr))
- (eval (car fptr)))))
-
- (defmacro TRACE-FORWARD-RULE (label datum rule-record bindings)
- ;; Print a trace of a forwards application of a rule, if turned on.
- `(if *rule-trace*
- #-:CCL (format *rule-trace* "~&~A: ~S -- ~S --> ~S"
- ,label ,datum (rule-record-rule-name ,rule-record)
- (substitute-bindings ,bindings
- (rule-record-pattern ,rule-record)))
- #+:CCL (rule-trace "~&~A: ~S -- ~S --> ~S"
- ,label ,datum (rule-record-rule-name ,rule-record)
- (substitute-bindings ,bindings
- (rule-record-pattern ,rule-record)))
- ))
-
- ) ; eval-when
-
- (defun SUBSTITUTE-BINDINGS-AND-LISP (bindings pattern &aux binding)
- ;; Like substitute-bindings in DNET, but also replaces :lisp with value.
- (declare (list bindings binding)
- (optimize (safety 1) (space 2) (speed 3)))
- (cond ((null pattern) nil)
- ((atom pattern)
- (if (and (variable-p pattern)
- (setf binding (assoc pattern bindings)))
- (cdr binding)
- pattern))
- ((eq (first pattern) :lisp)
- (lisp-escape bindings (rest pattern)))
- (T
- (cons (substitute-bindings-and-lisp bindings (car pattern))
- (substitute-bindings-and-lisp bindings (cdr pattern))))))
- (proclaim '(function substitute-bindings-and-lisp (list t) t))
-
- ;;;-----------------------------------------------------
- ;;; Applying the consequent. Returns T iff datum added.
-
- (defun DO-CONSEQUENT (pattern bindings data-dnet rule-name grounds)
- (declare (list pattern bindings) (symbol data-dnet rule-name))
- (case (car pattern)
- ((:LISP)
- (lisp-escape bindings (cdr pattern))
- nil)
- ((:DELETE)
- (delete-datum-internal
- (substitute-bindings-and-lisp bindings (second pattern)) data-dnet)
- nil)
- (otherwise
- (add-datum-internal
- (substitute-bindings-and-lisp bindings pattern)
- data-dnet rule-name grounds))))
- (proclaim '(function do-consequent (list list symbol symbol t) t))
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;; Forward Chaining
-
- ;;;-----------------------------
- ;;; One shot forward application
-
- (defun INFER-FROM-DATUM-INTERNAL (datum data-dnet rule-dnet)
- (declare (symbol data-dnet rule-dnet)
- (optimize (safety 1) (space 2) (speed 3)))
-
- ;; Plunk the datum into an antecedent template and find matching rules to run.
- (setf (cdr *antecedent-template*) datum)
- (multiple-value-bind
- (antecedents bindings)
- (dnet::match-expression-internal *antecedent-template* rule-dnet nil)
- (declare (list antecedents bindings))
- (do ((aptr antecedents (cdr aptr))
- (bptr bindings (cdr bptr)))
- ((null aptr))
- (declare (list aptr bptr))
-
- ;; Process each factored pattern in the rule's consequent.
- ;; Unrepeatable rules processed separately to test and save bindings.
- ;; (The code is repeated to avoid penalizing repeatables with tests.)
- (dolist (rule-record (dnet::expr-info-internal (first aptr) rule-dnet))
- (declare (list rule-record))
- (if (rule-record-repeatable rule-record)
- (let ((pattern (rule-record-pattern rule-record)))
- (declare (list pattern))
- (trace-forward-rule "F" datum rule-record (first bptr))
- (do-consequent pattern (first bptr)
- data-dnet (rule-record-rule-name rule-record) datum))
- (if (not (member (first bptr)
- (rule-record-bindings rule-record) :test #'equal))
- (let ((pattern (rule-record-pattern rule-record)))
- (declare (list pattern))
- (trace-forward-rule "F!" datum rule-record (first bptr))
- (do-consequent pattern (first bptr)
- data-dnet (rule-record-rule-name rule-record) datum)
- (push (first bptr) (rule-record-bindings rule-record)))))))))
-
- ;;;--------------------------------
- ;;; Forward Chaining on a Data Base DNET
-
- ;;; Separated for legibility, this does one pass of the forward chainer.
- ;;; Sets datum-added only if the indexer says a datum wasn't there before.
-
- (eval-when (eval compile)
-
- (defmacro FORWARD-CHAIN-PASS (end-list+rule-terminals data-dnet datum-added)
- `(dolist (end+terminal ,end-list+rule-terminals)
- (declare (cons end+terminal)
- (optimize (safety 1) (space 2) (speed 3)))
-
- ;; Get all the data that the rule succeeds on, and their bindings.
- (multiple-value-bind
- (grounds bindings)
- (get-matches ,data-dnet (cdr (dnet-terminal-expr (cdr end+terminal))) nil)
- (declare (list grounds bindings))
-
- ;; Iterate over the matching data and associated bindings ...
- (do ((gptr grounds (rest gptr))
- (bptr bindings (rest bptr)))
- ((null gptr))
- (declare (list gptr bptr))
-
- ;; There may be multiple consequents per antecedent. Execute each of
- ;; these consequents for the current matching datum and bindings.
- ;; Separate code for repeatable and unrepeatable so we don't penalize
- ;; speed of former for latter's tests.
- (dolist (rule-record (dnet-terminal-info (cdr end+terminal)))
- (declare (list rule-record))
- (if (rule-record-repeatable rule-record)
- (let ((pattern (rule-record-pattern rule-record)))
- (declare (list pattern))
- (trace-forward-rule "F" (first gptr) rule-record (first bptr))
- (if (do-consequent pattern (first bptr) data-dnet
- (rule-record-rule-name rule-record) (first gptr))
- (setf ,datum-added t)))
- (if (not (member (first bptr)
- (rule-record-bindings rule-record) :test #'equal))
- (let ((pattern (rule-record-pattern rule-record)))
- (declare (list pattern))
- (trace-forward-rule "F!" (first gptr) rule-record (first bptr))
- (if (do-consequent pattern (first bptr) data-dnet
- (rule-record-rule-name rule-record) (first gptr))
- (setf ,datum-added t))
- (push (first bptr) (rule-record-bindings rule-record))))))))))
-
- ) ; eval-when
-
- (defvariable ?::expr)
-
- (defun FORWARD-CHAIN-INTERNAL (data-dnet rule-dnet chain-bound)
- ;; Apply the batch forward chainer (which interprets :AND, etc) as long
- ;; as data are added, but not any more times than <chain-bound>.
- (declare (symbol data-dnet rule-dnet) (fixnum chain-bound)
- (optimize (safety 1) (space 2) (speed 3)))
- (do ((cycle 0 (1+ cycle))
- (datum-added t) ; get past entry
- (end-list+rule-terminals
- (dnet::pattern-match-links '(:antecedent . ?:expr)
- (list (dnet::dnet-link (sm:gets 'dnet rule-dnet))))))
- ((or (null datum-added) (> cycle chain-bound)) cycle)
- (declare (fixnum cycle) (list rule-terminals))
- (setf datum-added nil)
- (forward-chain-pass end-list+rule-terminals data-dnet datum-added)))
-
- ;;;------------------------------------------------------------------------
- ;;; GET-MATCHES is responsible for interpreting the special operators :LISP
- ;;; and :BIND, as well as for the base case of matching to the data base DNET.
- ;;; It uses MATCH-CONJUNCTS to deal with the interpretation of :AND and :SEQ, and
- ;;; splitting due to multiple matching data. Each call to GET-MATCHES gets
- ;;; an antecedent and a set of bindings as arguments, and returns an ordered
- ;;; list of grounds and a corresponding list of bindings, similar to the DNET
- ;;; match functions. A "ground" is whatever justified the success of the rule.
-
- (defun GET-MATCHES (data-dnet antecedent bindings)
- (declare (symbol data-dnet) (list antecedent bindings)
- (optimize (safety 1) (space 2) (speed 3)))
- (case (first antecedent)
-
- ;; :AND and :SEQ must succeed on all subcalls with consistent bindings.
- ((:and :seq) (match-conjuncts data-dnet (rest antecedent) nil bindings))
-
- ;; :LISP succeeds if evaluation does; no effect on bindings. The ground
- ;; is the value returned by lisp, so we can later figure out what happened.
- ((:lisp) (let ((lisp-result (lisp-escape bindings (cdr antecedent))))
- (if lisp-result
- (values (list lisp-result) (list bindings))
- (values nil nil))))
-
- ;; :BIND adds to current bindings if it is consistent, else fails. It
- ;; succeeds even if the lisp result is nil. (OK to return (nil) grounds.)
- ((:bind) (let ((lisp-result (lisp-escape bindings (cddr antecedent)))
- (prev-binding (assoc (second antecedent) bindings)))
- (if prev-binding
- (if (equal (cdr prev-binding) lisp-result)
- (values (list lisp-result) (list bindings))
- (values nil nil))
- (values (list lisp-result)
- (list (push (cons (second antecedent) lisp-result)
- bindings))))))
-
- ;; Anything else must match directly.
- (otherwise (dnet::match-pattern-internal antecedent data-dnet bindings))))
-
- ;;;------------------------------------------------------------------------
- ;;; MATCH-CONJUNCTS deals with two complications of forward chaining conjuncts:
- ;;; - :AND requires iterating over a sequence of antecedents to find a sequence
- ;;; of matching data with consistent bindings.
- ;;; - Matching one of these antecedents against a data dnet may produce more
- ;;; than one datum that matches. Whenever this happens, :AND processing must
- ;;; split, processing the remaining conjuncts in the context of each match.
-
- ;;; MATCH-CONJUNCTS is given some antecedents to consume, a grounds list of
- ;;; form (<ground1> ... <groundK>), and a single binding-set. Each element
- ;;; of the grounds list matches a corresponding antecedent which has already
- ;;; been consumed, with the indicated binding-set. In consuming the remaining
- ;;; antecedents, we may find several ways to do it. Hence we have to return
- ;;; a list of extensions and their binding sets, of form:
- ;;; ((<ground1> ... <groundK> <groundL>) ... (<ground1> ... <groundK> <groundM>))
- ;;; ( <binding-setL> <binding-setM> )
- ;;; [There may be more grounds added after <groundL> and <groundM>; this just
- ;;; gives the gist of it.] After the function splits on the first of the
- ;;; remaining antecedents, it calls itself on each element of this list, thus
- ;;; dividing the work, and unions the results.
-
- (defun MATCH-CONJUNCTS (data-dnet remaining-antecedents grounds bindings)
- (declare (symbol data-dnet) (list remaining-antecedents grounds bindings)
- (optimize (safety 1) (space 2) (speed 3)))
- (if (null remaining-antecedents)
-
- ;; No more remaining antecedents: the unextended grounds is returned.
- ;; (The extra recursive call before this base case is to cover (:AND).)
- (values (list grounds) (list bindings))
-
- ;; Get the grounds which match the next remaining antecedent. (The
- ;; get-matches call will use bindings to check consistency.)
- (multiple-value-bind
- (extensions extended-bindings)
- (get-matches data-dnet (first remaining-antecedents) bindings)
- (declare (list extensions extended-bindings))
-
- ;; Extend the ground with each match (the mapcar), and recurse separately
- ;; on each extended grounds to consume the rest of the remaining antecedents.
- (do ((gptr (mapcar #'(lambda (e) (append grounds (list e))) extensions)
- (rest gptr))
- (bptr extended-bindings (rest bptr))
- (result-grounds (list :head))
- (result-bindings (list :head)))
- ((null gptr) (values (rest result-grounds) (rest result-bindings)))
- (declare (list gptr bptr result-grounds result-bindings))
- (multiple-value-bind
- (new-grounds-list new-bindings-list)
- (match-conjuncts
- data-dnet (rest remaining-antecedents) (first gptr) (first bptr))
- (declare (list new-grounds-list new-bindings-list))
- (nconc result-grounds new-grounds-list)
- (nconc result-bindings new-bindings-list))))))
-
- ;;;------------------------------------------------------------
- ;;; Forward application which translates one DNET into another.
-
- (defun TRANSLATE-INTERNAL (source-dnet target-dnet rule-dnet)
- (declare (symbol source-dnet target-dnet rule-dnet)
- (optimize (safety 1) (space 2) (speed 3)))
-
- ;; Iterate over (:end-list . <dnet-terminal>)'s for each forward rule.
- (dolist (end+terminal (dnet::pattern-match-links
- '(:antecedent . ?:expr)
- (list (dnet::dnet-link (sm:gets 'dnet rule-dnet)))))
- (declare (cons end+terminal))
-
- ;; Get all the data that the rule succeeds on, and their bindings.
- (multiple-value-bind
- (grounds bindings)
- (get-matches source-dnet (cdr (dnet-terminal-expr (cdr end+terminal))) nil)
- (declare (list grounds bindings))
-
- ;; Iterate over the matching data and associated bindings ...
- (do ((gptr grounds (rest gptr))
- (bptr bindings (rest bptr)))
- ((null gptr))
- (declare (list gptr bptr))
-
- ;; There may be multiple consequents per antecedent. Execute each of
- ;; these consequents for the current matching datum and bindings.
- ;; (Repeatable option not processed in TRANSLATE.)
- (dolist (rule-record (dnet-terminal-info (cdr end+terminal)))
- (declare (list rule-record))
- (let ((pattern (rule-record-pattern rule-record)))
- (declare (list pattern))
- (trace-forward-rule "T" (first gptr) rule-record (first bptr))
- (do-consequent pattern (first bptr) target-dnet
- (rule-record-rule-name rule-record) (first gptr))))))))
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;;
- ;;; USER INTERFACE FUNCTIONS
- ;;;
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
- (defun INFER-FROM-DATUM (datum data-dnet rule-dnet)
- "infer-from-datum <datum> <data-dnet> <rule-dnet> [Function]
- Triggers whatever rules in <rule-dnet> immediately match <datum>, adding
- newly derived data to <data-dnet>. The warrant of the justification
- of each derived datum is the rule used to derive it, and the ground is
- the expression which matched the antecedent.
- This does NOT chain, nor does it interpret :AND, :SEQ, :LISP, or :BIND
- in the antecedent. :OR in the antecedent was handled when the rule was
- factored by ADD-RULE. It does interpret :LISP in the consequent, and
- :AND there was taken care of by ADD-RULE. This function is intended to
- be used in the INDEXPR-HOOK of data DNETs which do simple 'reflexive'
- reasoning on the addition of a datum, and don't need these extra frills.
- Chaining WILL result if the INDEXPR-HOOK of <rule-dnet> calls this
- INFER-FROM-DATUM on newly added data."
- ;; Why don't I just return a list of data, and let the user indexpr
- ;; them if desired? Because then the warrant & grounds would be lost.
-
- (declare (inline infer-from-datum-internal))
- (check-type data-dnet symbol)
- (check-type rule-dnet symbol)
- (assert (sm:gets 'dnet data-dnet) (data-dnet)
- "[DNET:INFER-FROM-DATUM] ~S is not a known DNET." data-dnet)
- (assert (sm:gets 'dnet rule-dnet) (rule-dnet)
- "[DNET:INFER-FROM-DATUM] ~S is not a known DNET." rule-dnet)
- (if *rule-trace* (format *rule-trace* "~&---------- Call to INFER-FROM-DATUM:"))
- (infer-from-datum-internal datum data-dnet rule-dnet))
-
- (defun FORWARD-CHAIN (data-dnet rule-dnet
- &optional (chain-limit most-positive-fixnum))
- "forward-chain <data-dnet> <rule-dnet> &optional chain-limit [Function]
- Derives all conclusions from <data-dnet> allowed by the forward rules
- in <rule-dnet>, and adds them to <data-dnet>. If new data are added,
- the process repeats. <Chain-limit> indicates how many times this may
- repeat: it defaults to MOST-POSITIVE-FIXNUM. The warrant of the
- justification of each derived datum is the rule used to derive it, and
- the ground is the expression which matched the antecedent, or the
- result of evaluation in the case of :LISP or :BIND. :AND and :SEQ are
- handled.
- NOTE that no attempt is made to synchronize rule firing. On each
- pass through the forward rules, later rules will see data added by
- earlier rules. Thus it is possible that rule chaining will occur in
- one 'pass', if they happen to be seen in the fortuitious order. You
- can aid this by entering rules into the DNET in the order in which
- they chain, eg. if rule-1 triggers rule-2 triggers rule3, add-rule
- them in this order. This saves re-testing rules that won't fire."
- (declare (inline forward-chain-internal))
- (check-type data-dnet symbol)
- (check-type rule-dnet symbol)
- (check-type chain-limit fixnum)
- (assert (sm:gets 'dnet data-dnet) (data-dnet)
- "[DNET:FORWARD-CHAIN] ~S is not a known DNET." data-dnet)
- (assert (sm:gets 'dnet rule-dnet) (rule-dnet)
- "[DNET:FORWARD-CHAIN] ~S is not a known DNET." rule-dnet)
- (if *rule-trace* (format *rule-trace* "~&---------- Call to FORWARD-CHAIN:"))
- (forward-chain-internal data-dnet rule-dnet chain-limit))
-
- (defun FORGET-PREVIOUS-BINDINGS (rule-dnet)
- "forget-previous-bindings <rule-dnet> [Function]
- Erases all memory of previous forward rule bindings, so :forward-unique
- filtering starts anew."
- (check-type rule-dnet symbol)
- (assert (sm:gets 'dnet rule-dnet) (rule-dnet)
- "[DNET:FORGET-PREVIOUS-BINDINGS] ~S is not a known DNET." rule-dnet)
- (map-dnet-terminals
- #'(lambda (dt)
- (map nil #'(lambda (rule-record)
- (declare (list rule-record))
- (unless (rule-record-repeatable rule-record)
- (setf (rule-record-bindings rule-record) nil)))
- (dnet-terminal-info dt)))
- rule-dnet))
-
- (defun TRANSLATE (source-dnet target-dnet rule-dnet)
- "translate <source-dnet> <target-dnet> <rule-dnet> [Function]
- Derives all conclusions from <source-dnet> allowed by the forward rules
- in <rule-dnet>, and adds them to <target-dnet>. Useful for rules that
- translate between representations."
- (declare (inline translate-internal))
- (check-type source-dnet symbol)
- (check-type target-dnet symbol)
- (check-type rule-dnet symbol)
- (assert (sm:gets 'dnet source-dnet) (source-dnet)
- "[DNET:TRANSLATE] ~S is not a known DNET." source-dnet)
- (assert (sm:gets 'dnet target-dnet) (target-dnet)
- "[DNET:TRANSLATE] ~S is not a known DNET." target-dnet)
- (assert (sm:gets 'dnet rule-dnet) (rule-dnet)
- "[DNET:TRANSLATE] ~S is not a known DNET." rule-dnet)
- (if *rule-trace* (format *rule-trace* "~&---------- Call to TRANSLATE:"))
- (translate-internal source-dnet target-dnet rule-dnet))
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- (provide :rule-forward)
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;; the end.